package demo.db.mybatis;

import java.io.IOException;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

import javax.sql.DataSource;

import org.apache.ibatis.cursor.Cursor;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.statement.SimpleStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.ibatis.transaction.Transaction;
import org.apache.ibatis.transaction.TransactionFactory;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.transaction.support.TransactionTemplate;

import demo.db.mybatis.dao.PostLoanInvestigationCaseMapper;
import demo.db.mybatis.dao.PostLoanInvestigationUserResultMapper;
import demo.db.mybatis.dao.UserMapper;
import demo.db.mybatis.pojo.PostLoanInvestigationCase;
import demo.db.mybatis.pojo.PostLoanInvestigationUserResult;
import demo.db.mybatis.pojo.User;
import demo.db.mybatis.service.UserService;
import demo.javax.sql.DataSourceDemo;

/**
 * <h2>简介</h2> MyBatis的前身叫iBatis，本是apache的一个开源项目, 2010年这个项目由apache software
 * foundation 迁移到了google code，并且改名为MyBatis。
 * MyBatis是支持普通SQL查询，存储过程和高级映射的优秀持久层框架。MyBatis消除了几乎所有的JDBC代码和参数的手工设置以及结果集的检索。
 * MyBatis使用简单的XML或注解用于配置和原始映射，将接口和Java的POJOs（Plain Old Java
 * Objects，普通的Java对象）映射成数据库中的记录。
 * <p>
 * Mybatis的功能架构分为三层
 * <li>1)
 * API接口层：提供给外部使用的接口API，开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。
 * <li>2) 数据处理层：负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。
 * <li>3)
 * 基础支撑层：负责最基础的功能支撑，包括连接管理、事务管理、配置加载和缓存处理，这些都是共用的东西，将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。
 * 
 * <h2>SqlSession</h2>
 * SqlSession对象的主要功能是完成一次数据库的访问和结果的映射，它类似于数据库的session概念，由于不是线程安全的，所以SqlSession对象的作用域需限制方法内。
 * SqlSession的默认实现类是DefaultSqlSession，它有两个必须配置的属性：Configuration和Executor。
 * SqlSession对数据库的操作都是通过Executor来完成的.
 * SqlSession有一个重要的方法getMapper，顾名思义，这个方式是用来获取Mapper对象的。
 * 
 * <h2>什么是Mapper对象？</h2>
 * 根据Mybatis的官方手册，应用程序除了要初始并启动Mybatis之外，还需要定义一些接口，接口里定义访问数据库的方法，存放接口的包路径下需要放置同名的XML配置文件。
 * SqlSession的getMapper方法是联系应用程序和Mybatis纽带，应用程序访问getMapper时，Mybatis会根据传入的接口类型和对应的XML配置文件生成一个代理对象，这个代理对象就叫Mapper对象。
 * 应用程序获得Mapper对象后，就应该通过这个Mapper对象来访问Mybatis的SqlSession对象，这样就达到里插入到Mybatis流程的目的。
 * 
 * <h2>Executor</h2>
 * 
 * Executor对象在创建Configuration对象的时候创建，并且缓存在Configuration对象里。
 * Executor对象的主要功能是调用StatementHandler访问数据库，并将查询结果存入缓存中（如果配置了缓存的话）。
 * 
 * <h2>StatementHandler</h2>
 * StatementHandler是真正访问数据库的地方，并调用ResultSetHandler处理查询结果。
 * 
 * <h2>ResultSetHandler</h2> 处理查询结果。
 * 
 * <h2>MyBatis缓存</h2> 正如大多数持久层框架一样，MyBatis 同样提供了一级缓存和二级缓存的支持
 * <li>一级缓存: 基于PerpetualCache 的 HashMap本地缓存，其存储作用域为 Session，当 Session flush 或
 * close 之后，该Session中的所有 Cache 就将清空。
 * <li>二级缓存与一级缓存其机制相同，默认也是采用 PerpetualCache，HashMap存储，不同在于其存储作用域为
 * Mapper(Namespace)，并且可自定义存储源，如 Ehcache。
 * <li>对于缓存数据更新机制，当某一个作用域(一级缓存Session/二级缓存Namespaces)的进行了 C/U/D 操作后，默认该作用域下所有
 * select 中的缓存将被clear。
 * 
 */
public class MyBatisDemo {

    public static String config = "db/mybatis/demo1/mybatis-config.xml";

    static Logger logger = LoggerFactory.getLogger(MyBatisDemo.class);

    public static void main(String[] args) throws Exception {
        // demoSpringMybatis();

        buildSqlSessionFactory(DataSourceDemo.local);
    }

    /**
     * 自定义CACHE,需要在Mapper文件里配置使用该自定义的缓存对象，如：
     * <cache type="demo.db.mybatis.cache.MyCache"/>
     * <p>
     * 可以看出，每次查询数据库前，MyBatis都会先在缓存中查找是否有该缓存对象。只有当调用了commit() 方法，MyBatis才会往缓存中写入数据。
     * <li>数据记录的键为 ：数字编号+Mapper名+方法名+SQL语句+参数
     * <p>
     * 格式，值为返回的对象值。
     * 
     */
    @Test
    public void specifyCache() throws IOException {
        SqlSessionFactory sqlSessionFactory = MybatisUtil.buildSqlSessionFactory(config);
        try (SqlSession session = sqlSessionFactory.openSession();) {
            User user = (User) session.selectOne("demo.db.mybatis.dao.UserMapperSpecifyCache.selectByPrimaryKey", 1);
            logger.info("user:{}", user);
            user = (User) session.selectOne("demo.db.mybatis.dao.UserMapperSpecifyCache.selectByPrimaryKey", 1);
            logger.info("user:{}", user);
        }
        try (SqlSession session = sqlSessionFactory.openSession();) {
            User user = (User) session.selectOne("demo.db.mybatis.dao.UserMapperSpecifyCache.selectByPrimaryKey", 1);
            logger.info("user:{}", user);
        }
    }

    /**
     * <h1>二级缓存:</h1>缓存在SqlSessionFactory层面给各个SqlSession 对象共享。默认二级缓存是不开启的，需要手动进行配置。
     * <li>使用二级缓存时，User类必须实现一个Serializable接口
     * <li>开启二级缓存，在userMapper.xml文件中添加如下配置
     * 
     * <pre>
      <mapper namespace="demo.db.mybatis.dao.UserMapper">
      <!-- 开启二级缓存 -->
      <cache/>
     * </pre>
     * 
     * <h2>二级缓存补充说明</h2>
     * <li>1. 映射语句文件中的所有select语句将会被缓存。
     * <li>2. 映射语句文件中的所有insert，update和delete语句会刷新缓存。
     * <li>3. 缓存会使用默认的Least Recently Used（LRU，最近最少使用原则）的算法来回收缓存空间
     * <li>4. 缓存会根据指定的时间间隔来刷新。
     * <li>5. 缓存会存储1024个对象
     * <li>6. 缓存会被视为是read/write（可读/可写）的缓存，
     * <h2>cache标签常用属性：</h2>
     * 
     * <pre>
      <cache 
    eviction="FIFO"  <!--回收策略为先进先出-->
    flushInterval="60000" <!--自动刷新时间60s-->
    size="512" <!--最多缓存512个引用对象-->
    readOnly="true"/> <!--只读-->
     * </pre>
     * 
     * <li>eviction：缓存回收策略 - LRU：最少使用原则，移除最长时间不使用的对象 - FIFO：先进先出原则，按照对象进入缓存顺序进行回收 -
     * SOFT：软引用，移除基于垃圾回收器状态和软引用规则的对象 - WEAK：弱引用，更积极的移除移除基于垃圾回收器状态和弱引用规则的对象
     * <li>flushInterval：刷新时间间隔，单位为毫秒，这里配置的100毫秒。如果不配置，那么只有在进行数据库修改操作才会被动刷新缓存区
     * <li>size：引用额数目，代表缓存最多可以存储的对象个数
     * <li>readOnly：是否只读，如果为true，则所有相同的sql语句返回的是同一个对象（有助于提高性能，但并发操作同一条数据时，可能不安全），如果设置为false，则相同的sql，后面访问的是cache的clone副本。
     * <li>useCache配置：​ 如果一条语句每次都需要最新的数据，就意味着每次都需要从数据库中查询数据，可以把这个属性设置为false，如：
     * <select id="selectAll" useCache="false">
     * <li>刷新缓存（就是清空缓存）：​ 二级缓存默认会在insert、update、delete操作后刷新缓存，可以手动配置不更新缓存，如：
     * <update id="updateById" flushCache="false" />
     */
    @Test
    public void cache2() throws IOException {
        SqlSessionFactory sqlSessionFactory = MybatisUtil.buildSqlSessionFactory(config);
        try (SqlSession session = sqlSessionFactory.openSession();) {
            User user = (User) session.selectOne("demo.db.mybatis.dao.UserMapper.selectByPrimaryKey", 1);
            logger.info("user:{}", user);
        }
        try (SqlSession session = sqlSessionFactory.openSession();) {
            User user = (User) session.selectOne("demo.db.mybatis.dao.UserMapper.selectByPrimaryKey", 1);
            logger.info("user:{}", user);
        }
    }

    /**
     * <h1>一级缓存</h1>: 也就Session级的缓存(默认开启) ，MyBatis 默认开启了一级缓存。一级缓存是在SqlSession
     * 层面进行缓存的。即，同一个SqlSession
     * ，多次调用同一个Mapper和同一个方法的同一个参数，只会进行一次数据库查询，然后把数据缓存到缓冲中，以后直接先从缓存中取出数据，不会直接去查数据库。 ​
     * 但是不同的SqlSession对象，因为不用的SqlSession都是相互隔离的，所以相同的Mapper、参数和方法，他还是会再次发送到SQL到数据库去执行，返回结果。
     * 
     */
    @Test
    public void cache1() throws IOException {
        SqlSessionFactory sqlSessionFactory = MybatisUtil.buildSqlSessionFactory(config);
        try (SqlSession session = sqlSessionFactory.openSession();) {
            User user = (User) session.selectOne("demo.db.mybatis.dao.UserMapper.selectByPrimaryKey", 1);
            logger.info("user:{}", user);
            logger.info("第二次请求时应该走的是缓存");
            User user2 = (User) session.selectOne("demo.db.mybatis.dao.UserMapper.selectByPrimaryKey", 1);
            logger.info("user:{}", user2);
            // Clears local session cache
            session.clearCache();
            logger.info("清理session cache后，再次请求时应该查库了");
            User user3 = (User) session.selectOne("demo.db.mybatis.dao.UserMapper.selectByPrimaryKey", 1);
            logger.info("user:{}", user3);
        }

        try (SqlSession session = sqlSessionFactory.openSession();) {
            logger.info("必须是同一个Session,如果session对象已经close()过了就不可能用了");
            User user = (User) session.selectOne("demo.db.mybatis.dao.UserMapper.selectByPrimaryKey", 1);
            logger.info("user:{}", user);
            logger.info("执行增删改的操作,这些操作都会清理缓存");
            user.setStatus((byte) 2);
            session.update("demo.db.mybatis.dao.UserMapper.updateByPrimaryKey", user);
            logger.info("再次查询时应该查库了");
            User user2 = (User) session.selectOne("demo.db.mybatis.dao.UserMapper.selectByPrimaryKey", 1);
            logger.info("user:{}", user2);
        }

    }

    /**
     * <h2>SqlSession</h2> 既然有了 SqlSessionFactory ，顾名思义，我们就可以从中获得 SqlSession
     * 的实例了。SqlSession 完全包含了面向数据库执行 SQL 命令所需的所有方法。 你可以通过 SqlSession实例来直接执行已映射的 SQL
     * 语句。
     * <p>
     * 在命名空间“demo.db.mybatis.dao.UserMapper”中定义了一个名为“selectByPrimaryKey”的映射语句，
     * 这样它就允许你使用指定的完全限定名“demo.db.mybatis.dao.UserMapper.selectByPrimaryKey”来调用映射语句。
     * <p>
     * 你可能注意到这和使用完全限定名调用 Java 对象的方法是相似的，之所以这样做是有原因的。这个命名可以直接映射到在命名空间中同名的 Mapper
     * 类，并将已映射的 select 语句中的名字、参数和返回类型匹配成方法。这样你就可以像上面那样很容易地调用这个对应 Mapper 接口的方法。
     * <p>
     * 
     */
    @Test
    public void demoSqlSession() throws IOException {
        SqlSessionFactory sqlSessionFactory = MybatisUtil.buildSqlSessionFactory(config);
        try (SqlSession session = sqlSessionFactory.openSession();) {
            User user = (User) session.selectOne("demo.db.mybatis.dao.UserMapper.selectByPrimaryKey", 1);
            logger.info("user:{}", user);
        }
    }

    /**
     * nameSpace命名空间使得你所见到的接口绑定成为可能。出于长远考虑，使用命名空间，并将它置于合适的
     * Java包命名空间之下，你将拥有一份更加整洁的代码并提高了 MyBatis 的可用性。
     * 命名解析：为了减少输入量，MyBatis对所有的命名配置元素（包括语句，结果映射，缓存等）使用了如下的命名解析规则。
     * <li>完全限定名（比如“com.mypackage.MyMapper.selectAllThings”）将被直接查找并且找到即用。
     * <li>短名称（比如“selectAllThings”）如果全局唯一也可以作为一个单独的引用。如果不唯一，有两个或两个以上的相同名称（比如“com.foo.selectAllThings
     * ”和“com.bar.selectAllThings”），那么使用时就会收到错误报告说短名称是不唯一的，这种情况下就必须使用完全限定名。
     * 
     */
    @Test
    public void testMapper() throws IOException {
        SqlSessionFactory sqlSessionFactory = MybatisUtil.buildSqlSessionFactory(config);
        try (SqlSession session = sqlSessionFactory.openSession();) {
            UserMapper mapper = session.getMapper(UserMapper.class);
            User user = mapper.selectByPrimaryKey(1);
            logger.info("user:{}", user);
            user = mapper.selectById(1);
            logger.info("user:{}", user);
        }
    }

    @Test
    public void testBatchInsert() throws IOException {
        SqlSessionFactory sqlSessionFactory = MybatisUtil.buildSqlSessionFactory(config);
        try (SqlSession session = sqlSessionFactory.openSession();) {
            UserMapper mapper = session.getMapper(UserMapper.class);
            User u1 = new User("N");
            User u2 = new User("X");
            int cnt = mapper.batchInsert(Arrays.asList(u1, u2));
            System.err.println("cnt = " + cnt);
            session.commit();
        }
    }

    @Test
    public void testCursor() throws IOException {
        SqlSessionFactory sqlSessionFactory = MybatisUtil.buildSqlSessionFactory(config);
        try (SqlSession session = sqlSessionFactory.openSession();) {
            UserMapper mapper = session.getMapper(UserMapper.class);

            try (Cursor<User> cursor = mapper.streamQuery();) {
                System.out.println("CurrentIndex = " + cursor.getCurrentIndex());
                cursor.forEach(t -> {
                    System.out.println(t);
                });
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 校验myBatis对Java8日期类的支持
     * 
     * @throws IOException
     */
    @Test
    public void localDateTime() throws IOException {
        SqlSessionFactory sqlSessionFactory = MybatisUtil.buildSqlSessionFactory(config);
        try (SqlSession session = sqlSessionFactory.openSession();) {
            PostLoanInvestigationCaseMapper mapper = session.getMapper(PostLoanInvestigationCaseMapper.class);
            PostLoanInvestigationCase record = new PostLoanInvestigationCase();
            record.setAdminId(2L);
            record.setCreateAt(LocalDateTime.now());
            int count = mapper.insertSelective(record);
            logger.info("========>count={}", count);
            // 如果没有commit则不提交事务
            session.commit();

            PostLoanInvestigationUserResultMapper mapper2 = session
                    .getMapper(PostLoanInvestigationUserResultMapper.class);
            PostLoanInvestigationUserResult record2 = new PostLoanInvestigationUserResult();
            record2.setAdminId(2L);
            record2.setCreateAt(new Date());
            record2.setAccountCreateAt(new Date());
            record2.setAdminId(1L);
            record2.setCaseId(1);
            record2.setLoanAt(LocalDateTime.now());
            record2.setLoanRepayDate(LocalDate.now());
            record2.setUserId(1L);
            count = mapper2.insertSelective(record2);
            logger.info("========>count={}", count);
            // 如果没有commit则不提交事务
            session.commit();
        }
    }

    /**
     * 如果你更愿意直接从 Java 程序而不是 XML 文件中创建 configuration，或者创建你自己的 configuration
     * 构建器，MyBatis 也提供了完整的配置类，提供所有和 XML 文件相同功能的配置项。
     * 
     * @param dataSource
     * @return
     */
    static SqlSessionFactory buildSqlSessionFactory(DataSource dataSource) {
        TransactionFactory transactionFactory = new JdbcTransactionFactory();
        Environment environment = new Environment("development", transactionFactory, dataSource);
        Configuration configuration = new Configuration(environment);
        /**
         * 注意该例中，configuration 添加了一个映射器类（mapper class）。映射器类是 Java 类，它们包含 SQL
         * 映射语句的注解从而避免了 XML 文件的依赖。 不过，由于 Java 注解的一些限制加之某些 MyBatis 映射的复杂性，XML
         * 映射对于大多数高级映射（比如：嵌套 Join 映射）来说仍然是必须的。 有鉴于此，如果存在一个对等的 XML 配置文件的话，MyBatis
         * 会自动查找并加载它（这种情况下， BlogMapper.xml 将会基于类路径和 BlogMapper.class 的类名被加载进来）。
         */
        configuration.addMapper(UserMapper.class);
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        return sqlSessionFactoryBuilder.build(configuration);
    }

    /**
     * 依赖注入框架可以创建线程安全的、基于事务的 SqlSession 和映射器（mapper）并将它们直接注入到你的 bean
     * 中，因此可以直接忽略它们的生命周期。 如果对如何通过依赖注入框架来使用 MyBatis感兴趣可以研究一下 MyBatis-Spring
     */
    static void demoSpringMybatis() {
        // 使用"spring.xml"和"spring-mybatis.xml"这两个配置文件创建Spring上下文
        ApplicationContext ac = new ClassPathXmlApplicationContext(
                new String[] { "spring/beans.xml", "spring/spring-mybatis.xml" });
        // 从Spring容器中根据bean的id取出我们要使用的userService对象
        UserService serviceA = ac.getBean(UserService.class);
    }

    /**
     * 
     * @param sqlSessionFactory
     * @throws SQLException
     */
    public static void demoConfiguration(SqlSessionFactory sqlSessionFactory) throws SQLException {
        Configuration configuration = sqlSessionFactory.getConfiguration();
        Executor executor = newExecutor(configuration);
        MappedStatement mappedStatement = null;
        Object parameter = null;
        RowBounds rowBounds = null;
        ResultHandler resultHandler = null;
        BoundSql boundSql = null;
        StatementHandler statementHandler = new SimpleStatementHandler(executor, mappedStatement, parameter, rowBounds,
                resultHandler, boundSql);

        Statement statement = null;
        statementHandler.query(statement, resultHandler);
    }

    static Executor newExecutor(Configuration configuration) {
        final Environment environment = configuration.getEnvironment();
        final DataSource dataSource = environment.getDataSource();
        final TransactionFactory transactionFactory = environment.getTransactionFactory();
        final Transaction transaction = transactionFactory.newTransaction(dataSource, null, false);
        return configuration.newExecutor(transaction, ExecutorType.SIMPLE);
    }

}
